package com.investmenttechnologyservices.services;

import static com.google.common.base.Preconditions.checkState;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
import com.google.appengine.api.datastore.Query.Filter;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.inject.Inject;
import com.investmenttechnologyservices.model.Bank;
import com.investmenttechnologyservices.model.Exchange;
import com.investmenttechnologyservices.model.HolidayDate;
import com.investmenttechnologyservices.model.Region;
import com.investmenttechnologyservices.util.DateUtil;

public class HolidayDateServiceImpl implements HolidayDateService {

	private class BusinessDayRequest {
		public Key key;
		public Date date;
		public Integer count;
		public Date result;
	}

	private class HolidayDatesRequest {
		List<Key> keys;
		List <Date> dates;
	}
	private static final Logger log = Logger.getLogger(HolidayDateServiceImpl.class.getName());
	
	private final DatastoreService ds;
	private final RegionService regionService;
	
	private BusinessDayRequest businessDayRequest = null; 
	private HolidayDatesRequest holidayDatesRequest = null;
	

	@Inject
	private HolidayDateServiceImpl(DatastoreService ds, RegionService regionService) {
		this.ds = ds;
		this.regionService = regionService;
	}

	@Override
	public Date addBusinessDays(Key key, Date date, int count) {
		
		log.info("[addBusinessDays(in): key="+key.toString()+", date="+date.toString()+
				", count="+count+"]");
		checkState(count>0);
		
		if (requestRepeated(key, date, count)) {
			log.info("[returning cached result: result="+businessDayRequest.result.toString()+"]");
			return businessDayRequest.result;
		}

		List<Key> keys = expandKey(key);

		Region region;
		Date result = date;
		try {
			region = new Region(ds.get(keys.get(0)));
			Long dow = region.getDow();
			
			List<Date> dates = getHolidayDates(keys);
			Integer j=0;
			Integer k = dates.size();
			while (k > 0 && j < k && dates.get(j).before(date)) {
				j++;
			}
			
			Integer i = 0;
			
			while (i < count) {
				result = DateUtil.addDays(result, 1);
				if (DateUtil.isWeekDay(dow, result)) {
					if (k>0 && j<k && DateUtil.toDateString(result)==DateUtil.toDateString(dates.get(j))){
						j++;
					} else {
						i++;
					}
				}
			}
		} catch (EntityNotFoundException ex) {
			// TODO Auto-generated catch block
			ex.printStackTrace();
		}
		
		setBusinessDayRequest(key, date, count, result);
		log.info("[addBusinessDays(out): date="+date.toString()+"]");
		return result;
	}

	
	@Override
	public Boolean isBusinessDay(Key key, Date date) {
		
		log.info("[isBusinessDay(in): key="+key.toString()+", date="+date.toString()+"]");

		Boolean businessDay = true;
		
		List<Key> keys = expandKey(key);
		
		try {
			Region region = new Region(ds.get(keys.get(0)));
			Long dow = region.getDow();

			if (!DateUtil.isWeekDay(dow, date)) {
				businessDay = false;
			} else {
				List<Date> dates = getHolidayDates(keys);
		
				Integer j = 0;
				Integer k = dates.size();
				if (k>0) {
					while (j < k && date.after(dates.get(j))) {
						j++;
					}
					if (j<k && dates.get(j).equals(date)) {
						businessDay = false;
					} 

				}
			}
		} catch (EntityNotFoundException ex) {
			// TODO Auto-generated catch block
			ex.printStackTrace();
		}
		
		log.info("[isBusinessDay(out): businessDay="+businessDay.toString()+"]");
		return businessDay;
		
	}


	private List<Date> getHolidayDates(List<Key> keys){
		log.info("[getHolidayDates(in): keys="+keys.toString()+"]");
		
		if (requestRepeated(keys)) {
			log.info("[getHolidayDates(out): dates(from cache)="+holidayDatesRequest.dates.toString()+"]");
			return holidayDatesRequest.dates;
		}
		
		Key regionKey 	= keys.get(0);
		Key exchangeKey = keys.get(1);
		Key bankKey 	= keys.get(2);
		
		List<Date> dates = new ArrayList<Date>();
		try {

			Query query;
			HolidayDate holidayDate;
			
			List<Key> regionKeys = regionService.getRegionChildren(regionKey);
			Filter queryFilter = new FilterPredicate("regionKey",FilterOperator.IN,regionKeys);

			if (exchangeKey != null) {
				queryFilter = CompositeFilterOperator.or(queryFilter,
						new FilterPredicate("exchangeKey",FilterOperator.EQUAL,exchangeKey));
			}
			
			if (bankKey != null) {
				queryFilter = CompositeFilterOperator.or(queryFilter,
						new FilterPredicate("bankKey",FilterOperator.EQUAL,bankKey));
			}

			FetchOptions fo = FetchOptions.Builder.withDefaults();
			query = new Query("holidayDate")
				.setFilter(queryFilter)
				.addSort("holidayDate", SortDirection.ASCENDING);

			List<Entity> result = ds.prepare(query).asList(fo);

			if (!result.isEmpty()) {
				Long dow = new Region(ds.get(regionKey)).getDow();
				Date newDate = null;
				
				for (Entity iter : result) {
					holidayDate = new HolidayDate(iter);
//					log.info("[retrieved holidays: HolidayDate="+holidayDate.toString()+"]");
					if (holidayDate.getRepeatYearly()){
						newDate = DateUtil.thisYear(holidayDate.getHolidayDate());
					} else {
						newDate = holidayDate.getHolidayDate();
					}
					if (newDate != null) {
						if (holidayDate.getMoveToMonday()) {
							newDate = DateUtil.nextWeekDay(dow, newDate);
						}
//						log.info("[added holidays: newDate="+newDate.toString()+"]");
						dates.add(newDate);
					}
				}
				Collections.sort(dates);
				setHolidayDatesRequest(keys, dates);
			}
		} catch (EntityNotFoundException ex) {
			// TODO Auto-generated catch block
			ex.printStackTrace();
		}
		log.info("[getHolidayDates(out): dates="+dates.toString()+"]");
		return dates;
	}
	
	private List<Key> expandKey(Key key){
		
		List<Key> keys = new ArrayList<Key>();

		String kind = key.getKind();
		Key regionKey;
		Key bankKey = null;
		Key exchangeKey = null;

		try {
			if (kind == "exchange") {
				exchangeKey = key;
				Exchange exchange;
				exchange = new Exchange(ds.get(key));
				regionKey = exchange.getRegionKey();
			} else if (kind == "bank") {
				bankKey = key;
				Bank bank = new Bank(ds.get(key));
				regionKey = bank.getRegionKey();
			} else {
				regionKey = key;
			}
			keys.add(regionKey);
			keys.add(exchangeKey);
			keys.add(bankKey);
		} catch (EntityNotFoundException ex) {
			// TODO Auto-generated catch block
			ex.printStackTrace();
		}
			
		return keys;
	}

	private void setBusinessDayRequest(Key key, Date date, Integer count, Date result) {
		if (businessDayRequest == null) {
			businessDayRequest = new BusinessDayRequest();
		}
		businessDayRequest.key = key;
		businessDayRequest.date = date;
		businessDayRequest.count = count;
		businessDayRequest.result = result;
	}

	private Boolean requestRepeated(Key key, Date date, Integer count) {
		if (businessDayRequest != null &&
			key 	== businessDayRequest.key &&
			date 	== businessDayRequest.date &&
			count 	== businessDayRequest.count) {
			return true;
		} else {
			return false;
		}
	}

	private Boolean requestRepeated(List<Key>keys) {
		if (holidayDatesRequest != null &&
			keys.get(0) 	== holidayDatesRequest.keys.get(0) &&
			keys.get(1) 	== holidayDatesRequest.keys.get(1) &&
			keys.get(2) 	== holidayDatesRequest.keys.get(2)) {
			return true;
		} else {
			return false;
		}
	}

	private void setHolidayDatesRequest(List<Key> keys, List<Date> dates) {
		if (holidayDatesRequest == null) {
			holidayDatesRequest = new HolidayDatesRequest();
		}
		holidayDatesRequest.keys = keys;
		holidayDatesRequest.dates = dates;
	}

}
